/*
 * Copyright 2024 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.pajamas.daydream.treeFlatFormat.json;

import com.google.common.collect.Maps;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;

import org.apache.commons.lang3.StringUtils;
import org.pajamas.daydream.treeFlatFormat.json.util.JsonPathUtils;
import org.pajamas.protocol.Wrapper;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Collections;
import java.util.Iterator;
import java.util.Map;
import java.util.Objects;

import javax.annotation.Nullable;

/**
 * @author william
 * @since 2024/1/3
 */

public class JacksonJsonTreeFlatFormatter extends AbstractJsonTreeFlatFormatter {
    private final transient ObjectMapper objectMapper;

    public JacksonJsonTreeFlatFormatter(@Nullable ObjectMapper objectMapper) {
        Objects.requireNonNull(objectMapper);
        this.objectMapper = objectMapper;
    }

    private static void assertRootIsObject(JsonNode rootNode) {
        if (!rootNode.isObject()) {
            throw new IllegalArgumentException("root node must be object");
        }
    }

    @Override
    public Map<String, Object> doFormat(String jsonString) {
        try {
            JsonNode pNode = this.objectMapper.readTree(jsonString);
            return this.format0(pNode);
        } catch (JsonProcessingException ex) {
            throw new IllegalArgumentException(ex);
        }
    }

    @Override
    public Map<String, Object> doFormat(File jsonFile) {
        try {
            JsonNode pNode = this.objectMapper.readTree(jsonFile);
            return this.format0(pNode);
        } catch (IOException ex) {
            throw new IllegalArgumentException(ex);
        }
    }

    @Override
    public Map<String, Object> doFormat(InputStream inputStream) {
        try {
            JsonNode pNode = this.objectMapper.readTree(inputStream);
            return this.format0(pNode);
        } catch (IOException ex) {
            throw new IllegalArgumentException(ex);
        }
    }

    @Override
    public Map<String, Object> doFormat(Object srcObj) {
        try {
            JsonNode pNode = this.objectMapper.valueToTree(srcObj);
            return this.format0(pNode);
        } catch (JsonProcessingException ex) {
            throw new IllegalArgumentException(ex);
        }
    }

    @Override
    public Map<String, Object> doFormat(URL url) {
        try {
            JsonNode pNode = this.objectMapper.readTree(url);
            return this.format0(pNode);
        } catch (IOException ex) {
            throw new IllegalArgumentException(ex);
        }
    }

    private Map<String, Object> format0(JsonNode rootNode) throws JsonProcessingException {
        if (rootNode == null) {
            return Collections.emptyMap();
        }
        assertRootIsObject(rootNode);
        Map<String, Object> flatMap = Maps.newHashMap();
        formatRecursively(flatMap, rootNode, "");
        return flatMap;
    }

    private void formatRecursively(Map<String, Object> flatMap, JsonNode pNode, String pPath) throws JsonProcessingException {
        pNode = unwrapIfNecessary(pNode);
        if (pNode.isObject()) {
            Iterator<Map.Entry<String, JsonNode>> iter = pNode.fields();
            while (iter.hasNext()) {
                Map.Entry<String, JsonNode> entry = iter.next();
                String cName = entry.getKey();
                JsonNode cNode = entry.getValue();
                String cPath = JsonPathUtils.joinJsonPath(pPath, cName);
                formatRecursively(flatMap, cNode, cPath);
            }
        } else if (pNode.isArray()) {
            ArrayNode arrayNode = (ArrayNode) pNode;
            String pattern = JsonPathUtils.prepareJsonArrayPattern(pPath);
            for (int idx = 0; idx < arrayNode.size(); idx++) {
                JsonNode cNode = arrayNode.get(idx);
                String cPath = JsonPathUtils.placeJsonArrayPath(pattern, idx);
                formatRecursively(flatMap, cNode, cPath);
            }
        } else if (pNode.isValueNode()) {
            Object value = this.objectMapper.treeToValue(pNode, Object.class);
            flatMap.put(pPath, value);
        }
    }

    @Deprecated
    private void formatRecursively(Map<String, Object> flatMap, JsonNode pNode, String pPath, String pName)
            throws JsonProcessingException {
        if (pNode == null || pNode.isEmpty() || StringUtils.isBlank(pName)) {
            return;
        }
        String cPath = JsonPathUtils.joinJsonPath(pPath, pName);

        if (pNode.isObject()) {
            Iterator<String> nameIterator = pNode.fieldNames();
            while (nameIterator.hasNext()) {
                String cName = nameIterator.next();
                JsonNode cNode = pNode.get(cName);
                this.formatRecursively(flatMap, cNode, cPath, cName);
            }
        } else {
            Object value = this.objectMapper.treeToValue(pNode, Object.class);
            flatMap.put(cPath, value);
        }
    }

    private JsonNode unwrapIfNecessary(JsonNode node) throws JsonProcessingException {
        Object value = this.objectMapper.treeToValue(node, Object.class);
        if (value != null) {
            if (Wrapper.class.isAssignableFrom(value.getClass())) {
                Object unwrapObject = ((Wrapper<?>) value).unwrap();
                return this.objectMapper.valueToTree(unwrapObject);
            }
        }
        return node;
    }
}
